<?php

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

namespace org\copperforest\lock;

class Lock
{

    const DEADLOCK_TIMEOUT = 10; //16 horas deberían ser más que suficiente? No para un avisador de timeout, Si esperamos esperar más de 10 minutos debería avisarnos
    const SLEEP_MICROTIME = 1000;

    static private $LOCK_PATH;
    //locks adquiridos durante la ejecucion
    static private $locks = array( );
    private $filename;
    private $lock_ex;
    private $lock_sh;
    private $lock_ex_sh;
    private $deep = 0;
    private $first_ex = 0;
    private $first_sh = 0;
    private $level = LOCK_UN;
    private $acquired = false;

    private function Lock( $name )
    {
        $this->filename = $name;

        //traduzco los directory separator para poder crear un nombre de archivo correcto
        $name = strtr( $name, '!', '!0' );
        $name = strtr( $name, DIRECTORY_SEPARATOR, '!1' );

        $this->lock_ex = self::$LOCK_PATH . $name . '.ex';
        $this->lock_sh = self::$LOCK_PATH . $name . '.sh';
        $this->lock_ex_sh = self::$LOCK_PATH . $name . '.ex_sh';
    }

    static function getInstance( $name )
    {
        if ( isset( self::$locks[ $name ] ) ) return self::$locks[ $name ];

        else {
            self::$locks[ $name ] = new Lock( $name );
            return self::$locks[ $name ];
        }
    }

    function exclusive( $wait = true )
    {

        //SOLO PUEDE HABER DOS CAMBIOS DE UN O SH A EX
        if ( $this->level != LOCK_EX ) {

            if ( $this->level == LOCK_SH ) {
                //Si ya tengo un bloqueo compartido debería eliminar el sh si es posible
                //si no es posible descender uno el numero de procesos bloqueandolo 
                //tendría que dejar el compartido, para ello tengo que acceder en exclusiva al fichero ex_sh
                if ( $wait ) {

                    $sleeptime = Lock::SLEEP_MICROTIME; //1ms
                    //Evito que nadie mas reclame la seccion critica
                    while ( !@mkdir( $this->lock_ex_sh ) ) {
                        usleep( $sleeptime );
                        $sleeptime = $sleeptime * mt_rand( 1, 2 );
                    }

                    $num = file_get_contents( $this->lock_sh ) - 1;

                    if ( $num == 0 ) unlink( $this->lock_sh );
                    else
                            file_put_contents( $this->lock_sh, $num ); //decremento para que los otros procesos acaben eliminando el archivo

                    rmdir( $this->lock_ex_sh );
                }
                else if ( @mkdir( $this->lock_ex_sh ) ) {

                    if ( file_get_contents( $this->lock_sh ) == 1 ) {
                        unlink( $this->lock_sh );
                        rmdir( $this->lock_ex_sh );
                    } else {
                        rmdir( $this->lock_ex_sh );
                        return false;
                    }
                }
            }

            if ( $wait ) {

                //no lo hagamos ahora total vamos a tener que esperar un ciento
                if ( file_exists( $this->lock_ex ) && ( filemtime( $this->lock_ex ) < $_SERVER[ 'REQUEST_TIME' ] - self::DEADLOCK_TIMEOUT ) )
                        throw new DeadLockTimeoutException( $this->filename,
                    $_SERVER[ 'REQUEST_TIME' ] - filemtime( $this->lock_ex ) );

                $sleeptime = Lock::SLEEP_MICROTIME; //1ms
                //Evito que nadie mas reclame la seccion critica
                while ( !@mkdir( $this->lock_ex ) ) {
                    usleep( $sleeptime );
                    $sleeptime = $sleeptime * mt_rand( 1, 2 );
                }

                //LO QUE SI PUEDO HACER ES LANZAR UNA EXCEPCION DE SEGURIRAD SI SUPERA ALGUN TIPO DE TIEMPO
                if ( file_exists( $this->lock_sh ) && ( filemtime( $this->lock_sh ) < $_SERVER[ 'REQUEST_TIME' ] - self::DEADLOCK_TIMEOUT ) ) {
                    rmdir( $this->lock_ex );
                    throw new DeadLockTimeoutException( $this->filename,
                    $_SERVER[ 'REQUEST_TIME' ] - filemtime( $this->lock_sh ) );
                }

                $sleeptime = Lock::SLEEP_MICROTIME; //1ms
                //
                //Espero a que desbloqueen todos los shared
                while ( file_exists( $this->lock_sh ) ) {
                    usleep( $sleeptime );
                    $sleeptime = $sleeptime * mt_rand( 1, 2 );
                }
            } else if ( !@mkdir( $this->lock_ex ) ) {

                return false;
            } else if ( file_exists( $this->lock_sh ) ) {
                unlink( $this->lock_ex );
                return false;
            }
        }

        $this->up( LOCK_EX );
        return true;
    }

    function shared( $wait = true )
    {

        if ( $this->level == LOCK_UN ) { //solo si no esta bloqueado hacemos algo
            if ( $wait ) {

                if ( file_exists( filemtime( $this->lock_ex ) ) && ( filemtime( $this->lock_ex ) < $_SERVER[ 'REQUEST_TIME' ] - self::DEADLOCK_TIMEOUT ) )
                        throw new DeadLockTimeoutException( $this->filename,
                    $_SERVER[ 'REQUEST_TIME' ] - filemtime( $this->lock_ex ) );

                $sleeptime = Lock::SLEEP_MICROTIME; //1ms
                //Si no hay un bloqueo exclusivo pendiente
                while ( !@mkdir( $this->lock_ex ) ) {
                    usleep( $sleeptime );
                    $sleeptime = $sleeptime * mt_rand( 1, 2 );
                }

                if ( file_exists( filemtime( $this->lock_ex ) ) && ( filemtime( $this->lock_ex_sh ) < $_SERVER[ 'REQUEST_TIME' ] - self::DEADLOCK_TIMEOUT ) ) {
                    unlink( $this->lock_ex );
                    throw new DeadLockTimeoutException( $this->filename,
                    $_SERVER[ 'REQUEST_TIME' ] - filemtime( $this->lock_ex_sh ) );
                }

                $sleeptime = Lock::SLEEP_MICROTIME; //1ms
                //Inicio una sección crítica para editaar el .sh
                while ( !@mkdir( $this->lock_ex_sh ) ) {
                    usleep( $sleeptime );
                    $sleeptime = $sleeptime * mt_rand( 1, 2 );
                }


                if ( file_exists( $this->lock_sh ) )
                        file_put_contents( $this->lock_sh,
                            ( ( int ) file_get_contents( $this->lock_sh ) ) + 1 );
                else file_put_contents( $this->lock_sh, 1 );

                rmdir( $this->lock_ex_sh );
                //libero exclusivo para que lo pueda reclamar un bloqueo exclusivo
                rmdir( $this->lock_ex );
            }
            else if ( !@mkdir( $this->lock_ex ) ) //necesito asegurar que no hay ningun exclusivo esperando
                    return false;

            else if ( !@mkdir( $this->lock_ex_sh ) ) {
                rmdir( $this->lock_ex );

                return false;
            } else {

                //ahora no van a entrar otros bloqueos, pero sí pueden liberarse, por lo cual al liberarse deberían tb crear .ex
                if ( file_exists( $this->lock_sh ) )
                        file_put_contents( $this->lock_sh,
                            ( ( int ) file_get_contents( $this->lock_sh ) ) + 1 );
                else file_put_contents( $this->lock_sh, 1 );

                rmdir( $this->lock_ex_sh );
                rmdir( $this->lock_ex );
            }
        }

        $this->up( LOCK_SH );
        return true;
    }

    function release()
    {

        if ( $this->acquired ) {

            if ( $this->level == LOCK_EX ) {


                //SI el anterior nivel de bloqueo era shared, entonces tengo que prepeparar un bloqueo compartido

                $this->down();

                if ( $this->level !== LOCK_EX ) {



                    if ( $this->level == LOCK_SH ) {

                        /* $sleeptime = Lock::SLEEP_MICROTIME ; //1ms
                          while ( !@mkdir( $this->lock_ex_sh ) ) {
                          usleep( $sleeptime ) ;
                          $sleeptime = $sleeptime * mt_rand(1, 2) ;
                          } */

                        file_put_contents( $this->lock_sh, '1' );
                    }

                    rmdir( $this->lock_ex );
                }
            } else {

                $this->down();

                if ( $this->level == LOCK_UN ) { //si tiene un bloqueo exclusivo no hago nada
                    $sleeptime = Lock::SLEEP_MICROTIME; //1ms
                    //Inicio una sección crítica para editaar el .sh
                    while ( !@mkdir( $this->lock_ex_sh ) ) {
                        usleep( $sleeptime );
                        $sleeptime = $sleeptime * mt_rand( 1, 2 );
                    }

                    $num = file_get_contents( $this->lock_sh );

                    if ( $num == 1 ) rmdir( $this->lock_sh );
                    else file_put_contents( $this->lock_sh, $num - 1 );


                    @rmdir( $this->lock_ex_sh );
                }
            }
        }

        return !$this->acquired;
    }

    function releaseAll()
    {

        //Ahora tengo que ver si tengo un bloqueo exclusivo
        if ( $this->first_ex > 0 ) {
            rmdir( $this->lock_ex );
        } else if ( $this->first_sh > 0 ) {

            $sleeptime = Lock::SLEEP_MICROTIME; //1ms
            //Inicio una sección crítica para editaar el .sh
            while ( !@mkdir( $this->lock_ex_sh ) ) {
                usleep( $sleeptime );
                $sleeptime = $sleeptime * mt_rand( 1, 2 );
            }


            if ( file_get_contents( $this->lock_sh ) == 1 )
                    unlink( $this->lock_sh );
            else {

                $sleeptime = Lock::SLEEP_MICROTIME; //1ms
                //
                //Esto sobra ya está arriba
                /* while ( !@mkdir( $this->lock_ex_sh ) ) {
                  usleep( $sleeptime ) ;
                  $sleeptime = $sleeptime * mt_rand(1, 2) ;
                  } */

                $num = file_get_contents( $this->lock_sh );

                if ( $num == 1 ) unlink( $this->lock_sh );
                else file_put_contents( $this->lock_sh, $num - 1 );

                //tb sobra
                // @rmdir( $this->lock_ex_sh ) ;
            }

            @rmdir( $this->lock_ex_sh );
        }

        $this->deep = 0;
        $this->first_sh = 0;
        $this->first_ex = 0;
    }

    private function up( $level )
    {

        $this->deep++;

        if ( $level == LOCK_EX ) { //da igual que tenia antes
            if ( $this->first_ex == 0 ) {

                $this->first_ex = $this->deep;
                $this->level = LOCK_EX;
            }
        } else if ( $level == LOCK_SH ) {

            if ( $this->first_ex == 0 ) {

                if ( $this->first_sh == 0 ) {

                    $this->first_sh = $this->deep;
                    $this->level = LOCK_SH;
                }
            }
        }

        $this->acquired = true;
    }

    private function down()
    {

        if ( $this->first_sh == $this->deep ) //ESTO SOLO DEBIERA OCURRIR CON DEEP IGUAL A 1
                $this->level = LOCK_UN;

        else if ( $this->first_ex == $this->deep ) {

            if ( $this->first_sh == 0 ) $this->level = LOCK_UN;
            else $this->level = LOCK_SH;
        }

        $this->deep--;

        $this->acquired = $this->deep > 0;
    }

    static function __init()
    {
        self::$LOCK_PATH = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'ct_lock' . DIRECTORY_SEPARATOR;

        if ( !file_exists( self::$LOCK_PATH ) && !@mkdir( self::$LOCK_PATH ) ) {

            if ( defined( 'CF_ZERO_PATH' ) && file_exists( CF_ZERO_PATH ) )
                    self::$LOCK_PATH = CF_ZERO_PATH . 'lock' . DIRECTORY_SEPARATOR;

            else {
                
            }
        }
    }

    static function __destroy()
    {
        echo "Eliminando bloqueos";

        foreach ( self::$locks as $name => $object ) $object->releaseAll();
    }

}

Lock::__init();

register_shutdown_function( array( 'Lock', '__destroy' ) );
